﻿/*
 *  jMiny (c) 2012 Dan Nichols
 *  jMiny may be feely distributed under the MIT license.
 *
 *  Animation code based on Zepto.js (c) 2010-2012 Thomas Fuchs
 *  Zepto.js may be freely distributed under the MIT license.
 */
(function() {

  var UNDEFINED,
      REGEX_ID = /^#(\w+)$/,
      REGEX_CLASS = /^\.(\w+)$/,
      REGEX_TAG = /^(\w+)$/,
      REGEX_ATTR = /^\[(\w+)(\=(("([^"]*)")|('([^']*)')|((\w*))))?\]$/,
      REGEX_ATTR_ENDSWITH = /^\[(\w+)\$\=(("([^"]*)")|('([^']*)')|((\w*)))\]$/,
      REGEX_ATTR_STARTSWITH = /^\[(\w+)\^\=(("([^"]*)")|('([^']*)')|((\w*)))\]$/,
      REGEX_MARKUP = /^\<\w+\>/,
      REGEX_COMPLEX_SELECTOR = /^(\w+|\*)?((\.\w+)|(#\w+)|(\[\w+(\^|\$)?\=('[^']+')|("[^"]+")|(\w+)]))+/,
      REGEX_FIRST = /\S+\:first$/,
      SEL_HTML = 'html',
      SEL_BODY = 'body',
      _old$ = window['$'],
      _oldNS = window['jMiny'],
      _dataCacheId = Math.random(),
      _uuid = 0,
      _prefix = '',
      _transitionEndEventType = 'transitionend';

  (function() {
    var eventPrefixes = {Webkit: 'webkit', Moz: '', O: 'o', ms: 'MS'},
        elem = document.createElement('div'),
        vendor,
        eventPrefix;
    for (vendor in eventPrefixes) {
      if (elem.style[vendor + 'TransitionProperty'] !== UNDEFINED) {
        _prefix = '-' + vendor.toLowerCase() + '-';
        if (eventPrefix = eventPrefixes[vendor]) {
          _transitionEndEventType = eventPrefix + 'TransitionEnd';
        }
        break;
      }
    }
  })();

  function _isNS(selector) {
    return selector && selector instanceof jMiny;
  }
  
  function _isHtml(selector) {
    return selector && REGEX_MARKUP.test(selector);
  }
  
  function _isElement(selector) {
    return selector && selector instanceof Element;
  }
  
  function _isElementList(selector) {
    return selector instanceof Array && _isElement(selector[0]); 
  }
  
  function _isDescendantOf(elem, root) {
    if (elem) {
      var other;
      while (other = elem.parentNode) {
        if (other === root) {
          return true;
        }
        other = elem.parentNode;
      }
    }
    return false;
  }
  
  function _findInDOM(selector, rootElem) {
    rootElem = rootElem || document.documentElement;
    var elem,
      result;
    if (REGEX_ID.test(selector)) {
      if (arguments.length == 2) {
        elem = rootElem.querySelector(selector);
      } else {
        elem = document.getElementById(selector.slice(1));
      }
      result = elem ? [elem] : [];
    } else if (REGEX_CLASS.test(selector)) {
      result = jMiny.toArray(rootElem.getElementsByClassName(selector.slice(1)));
    } else if (SEL_BODY === selector && rootElem === document.documentElement) {
      result = [document.body];
    } else if (SEL_HTML === selector && rootElem === document.documentElement) {
      result = [document.documentElement];
    } else if (REGEX_TAG.test(selector)) {
      result = jMiny.toArray(rootElem.getElementsByTagName(selector));
    } else if (REGEX_FIRST.test(selector)) {
      elem = rootElem.querySelector(selector.slice(0, -6));
      result = elem ? [elem] : [];
    } else {
      result = jMiny.toArray(rootElem.querySelectorAll(selector));
    }
    return result;
  }
  
  function _matchesSelector(elem, selector) {
    if (REGEX_ID.test(selector)) {
      return elem.id === selector.slice(1);
    }
    else if (REGEX_CLASS.test(selector)) {
      var classNames = elem.className.split(' '),
          i = -1,
          className;
      selector = selector.slice(1);
      while (className = classNames[++i]) {
        if (className === selector) {
          return true;
        }
      }
    } else if (REGEX_TAG.test(selector)) {
      return elem.nodeName === selector.toUpperCase();
    } else if (REGEX_ATTR.test(selector)) {
      var values = REGEX_ATTR.exec(selector),
          attrName = values[1],
          attrValue = values[5],
          elemValue;
      if (attrValue === UNDEFINED) {
        attrValue = values[7];
      }
      if (attrValue === UNDEFINED) {
        attrValue = values[9];
      }
      if (elem.hasAttribute(attrName)) {
        if (UNDEFINED === attrValue) {
          return true;
        } else {
          return elem.getAttribute(attrName) === attrValue;
        }
      }
    } else if (REGEX_ATTR_ENDSWITH.test(selector)) {
      var values = REGEX_ATTR_ENDSWITH.exec(selector),
          attrName = values[1],
          attrValue = values[4] || values[6] || values[8],
          elemValue = elem.getAttribute(attrName);
      return elemValue && elemValue.slice(-attrValue.length) === attrValue;
    } else if (REGEX_ATTR_STARTSWITH.test(selector)) {
      var values = REGEX_ATTR_ENDSWITH.exec(selector),
          attrName = values[1],
          attrValue = values[4] || values[6] || values[8],
          elemValue = elem.getAttribute(attrName);
      return elemValue && elemValue.indexOf(attrValue) === 0;
    } else if ('*' === selector) {
      return true;
    } else if (REGEX_COMPLEX_SELECTOR.test(selector)) {
      var values = [],
        type,
        i = -1,
        c,
        s = '';
      while (c = selector.charAt(++i)) {
        if (c == '[' || c == '.' || c == '#') {
          if (s) {
            values[values.length] = s;
          }
          s = c;
        } else {
          s += c;
        }
      }
      if (s) {
        values[values.length] = s;
      }
      i = -1;
      while (c = values[++i]) {
        if (!_matchesSelector(elem, c)) {
          return false;
        }
      }
      return true;
    } else {
      var matching = jMiny.toArray(document.querySelectorAll(selector)),
        i = -1,
        matchingElem;
      while (matchingElem = matching[++i]) {
        if (matchingElem === elem) {
          return true;
        }
      }
    }
    return false;
  }
  
  function _create(html) {
    var div = document.createElement('div'),
        result = [],
        child;
    div.innerHTML = html;
    child = div.firstChild;
    while (child) {
      if (child.nodeType === 1) {
        result[result.length] = child;
      }
      child = child.nextSibling;
    }
    return result;
  }
  
  function _translateCSSPropertyName(prop) {
    prop = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
    if (/^(webkit-|moz-|ms-|o-|khtml-)/.test(prop)) {
      prop = '-' + prop;
    }
    return prop;
  }
  
  function _defaultTo(value, defaultValue) {
    return value === UNDEFINED ? value : defaultValue;
  }
  
  function _delegatedCallback(delegate, callback) {
    function result(e) {
      var target = e.target,
          root = e.currentTarget;
      while (target && target != root) {
        if (_matchesSelector(target, delegate)) {
          break;
        } else {
          target = target.parentNode;
          if (target == root) {
            target = null;
            break;
          }
        }
      }
      if (target) {
        var evt = new ProxyEvent(this, e.timestamp, e);
        callback.call(target, e);
      }
    }
    if (callback.guid === UNDEFINED) {
      callback.guid = _addEventListener.guid++;
    }
    result.delegate = callback.delegate = delegate;
    result.guid = callback.guid;
    return result;
  }
  
  function _addEventListener(elem, type, delegate, callback, reps) {
    var handler = elem['$on' + type];
    if (!handler) {
      elem['$on' + type] = handler = function(e) {
        var evt = new ProxyEvent(this, new Date().getTime(), e || window.event),
            markedToRemove = [],
            i = -1,
            cb;
        while (cb = arguments.callee.callbacks[++i]) {
          cb.call(this, evt);
          if (cb.reps !== UNDEFINED) {
            if (--cb.reps < 1) {
              markedToRemove[markedToRemove.length] = cb;
            }
          }
        }
        i = -1;
        while (cb = markedToRemove[++i]) {
          _removeEventListener(this, type, delegate, callback);
        }
      };
      if (elem.attachEvent) {
        elem.attachEvent('on' + type, handler);
      } else {
        elem.addEventListener(type, handler, false);
      }
    }
    if (!handler.callbacks) {
      handler.callbacks = [];
    }
    if (!callback.guid) {
      callback.guid = arguments.callee.guid++;
    }
    if (delegate) {
      callback = _delegatedCallback(delegate, callback);
    }
    if (reps !== UNDEFINED) {
      callback.reps = reps;
    }
    handler.callbacks.push(callback);
    elem = callback = null;
  }
  _addEventListener.guid = 0;
  
  function _removeEventListener(elem, type, delegate, callback) {
    var handler = elem['$on' + type],
        i = -1,
        cb;
    if (handler && handler.callbacks) {
      if (!callback) {
        handler.callbacks.length = 0;
      } else {
        while (cb = handler.callbacks[++i]) {
          if (cb.guid == callback && (!delegate || callback.delegate == delegate)) {
            delete handler.callbacks[i];
            break;
          }
        }
      }
    }
  }
  
  function ProxyEvent(currentTarget, timestamp, e) {
    this._ = e;
    this.currentTarget = currentTarget;
    this.target = e.target || e.srcElement;
    this.keyCode = typeof e.keyCode == 'number' ? e.keyCode : typeof e.which == 'number' ? e.which : typeof e.charCode == 'number' ? e.charCode : null;
    this.relatedTarget = e.relatedTarget || (e.fromElement == this.target ? e.toElement : e.fromElement);
    this.timestamp = timestamp;
    var same = ['altKey', 'ctrlKey', 'shiftKey', 'clientX', 'clientY', 'pageX', 'pageY', 'screenX', 'screenY', 'offsetX', 'offsetY', 'type'],
        i = same.length;
    while (--i > -1) {
      this[same[i]] = e[same[i]];
    }
  }
  ProxyEvent.prototype = {
    preventDefault: function() {
      if (this._.preventDefault) {
        this._.preventDefault();
      } else {
        this._.returnValue = false;
      }
    },
    stopPropagation: function() {
      if (this._.stopPropagation) {
        this._.stopPropagation();
      } else {
        this._.cancelBubble = true;
      }
    }
  };
  
  function _doNothing() {
  }
  
  function _cloneElement(elem, deepCopy) {
    var clone = elem.cloneNode(deepCopy || false),
        prop;
    for (prop in elem) {
      if (prop.indexOf('on') == 0 || prop.indexOf('$') == 0) {
        clone[prop] = elem[prop];
      }
    }
    return clone;
  }
  
  function _domManip(newSet, referenceSet, callback) {
    referenceSet = jMiny(referenceSet);
    newSet = jMiny(newSet);
    var resultSet = [],
        i = -1,
        j,
        referenceElem,
        newElem,
        resultElem;
    while (referenceElem = referenceSet[++i]) {
      j = -1;
      while (newElem = newSet[++j]) {
        if (newElem.parentNode) {
          newElem.parentNode.removeChild(newElem);
        }
        resultElem = callback(newElem, referenceElem, referenceSet.length == 1 && newSet.length == 1 ? newElem : _cloneElement(newElem, true));
        if (resultElem) {
          resultSet[resultSet.length] = resultElem;
        }
      }
    }
    return jMiny(resultSet);
  }
  
  function _appendElement(newElem, referenceElem, clonedElem) {
    referenceElem.appendChild(clonedElem);
    return clonedElem;
  }
  
  function _prependElement(newElem, referenceElem, clonedElem) {
    clonedElem.insertBefore(referenceElem.firstChild);
    return clonedElem;
  }
  
  function _insertBeforeElement(newElem, referenceElem, clonedElem) {
    if (referenceElem.parentNode) {
      referenceElem.parentNode.insertBefore(clonedElem, referenceElem);
      return clonedElem;
    } else {
      return null;
    }
  }
  
  function _insertAfterElement(newElem, referenceElem, clonedElem) {
    if (referenceElem.parentNode) {
      referenceElem.parentNode.insertAfter(clonedElem, referenceElem);
      return clonedElem;
    } else {
      return null;
    }
  }
  
  function _replaceElement(newElem, referenceElem, clonedElem) {
    if (referenceElem.parentNode) {
      referenceElem.parentNode.replaceChild(clonedElem, referenceElem);
      return clonedElem;
    } else {
      return null;
    }
  }

  /**
   * @class jMiny
   * @desc A jQuery-like object
   *
   * @signature Creates a new jMiny object from a selector
   * @param<String> selector A CSS selector
   *
   * @signature Creates a new jMiny object from a DOM element
   * @param<Element> element A DOM element
   *
   * @signature Creates a new jMiny object from a list of DOM elements
   * @param<Array> elements A list of DOM elements
   *
   * @signature Creates a new jMiny object from another jMiny object
   * @param<jMiny> other A jMiny object
   *
   * @signature Creates a new jMiny object from a selector
   * @param<String> selector A CSS selector
   * @param<String> root A CSS selector for the elements to which the query is limited
   *
   * @signature Creates a new jMiny object from a selector
   * @param<String> selector A CSS selector
   * @param<Element> root A root element to which the query is limited
   *
   * @signature Creates a new jMiny object from a selector
   * @param<String> selector A CSS selector
   * @param<jMiny> root A jMiny object to which the query is limited
   */
  function jMiny(selector, root) {
    if (this instanceof jMiny) {
      if (selector) {
        this.add(selector, root);
      }
    } else {
      return new jMiny(selector, root);
    }
  };
  jMiny.prototype = {
    /**
     * @field<Number> length The number of elements in the jMiny object
     */
    length: 0,
    /**
     * @method toString
     *
     * @signature Creates a string representation of the jMiny object
     * @return<String> The string
     */
    toString: function() {
      return '<jMiny> ' + this.toArray().toString();
    },
    /**
     * @method toArray
     *
     * @signature Creates an array containing all elements from the jMiny object
     * @return<Array> The array
     */
    toArray: function() {
      return Array.prototype.slice.call(this, 0);
    },
    // Selection/Traversal
    /**
     * @method add
     *
     * @signature Adds one or more elements to this collection
     * @param<String> selector A CSS selector
     * @return<jMiny> The jMiny collection
     *
     * @signature Adds one or more elements to this collection
     * @param<Element> element A DOM element
     * @return<jMiny> The jMiny collection
     *
     * @signature Adds one or more elements to this collection
     * @param<Array> elements A list of DOM elements
     * @return<jMiny> The jMiny collection
     *
     * @signature Adds one or more elements to this collection
     * @param<jMiny> other A jMiny object
     * @return<jMiny> The jMiny collection
     *
     * @signature Adds one or more elements to this collection
     * @param<String> selector A CSS selector
     * @param<String> root A CSS selector for the elements to which the query is limited
     * @return<jMiny> The jMiny collection
     *
     * @signature Adds one or more elements to this collection
     * @param<String> selector A CSS selector
     * @param<Element> root A root element to which the query is limited
     * @return<jMiny> The jMiny collection
     *
     * @signature Adds one or more elements to this collection
     * @param<String> selector A CSS selector
     * @param<jMiny> root A jMiny object to which the query is limited
     * @return<jMiny> The jMiny collection
     */
    add: function(selector, root) {
      var elems,
          i;
      if (_isNS(selector)) {
        elems = selector;
      } else {
        o = new jMiny();
        if (_isHtml(selector)) {
          elems = _create(selector);
        } else if (_isElement(selector)) {
          elems = [selector];
        } else if (_isElementList(selector)) {
          elems = selector;
        } else if (typeof selector == 'string') {
          elems = _findInDOM(selector, root);
        } else {
          elems = [];
        }
        for (i = this.length + elems.length - 1; i >= this.length; i--) {
          this[i] = elems[i];
        }
        this.length += elems.length;
      }
      return this;
    },
    /**
     * @method children
     *
     * @signature Collects the immediate descendants of every element in this jMiny
     * @return<jMiny> The direct descendants
     *
     * @signature Collects the immediate descendants of every element in this jMiny matching a filter
     * @param<String> selector A CSS selector which the children must match
     * @return<jMiny> The direct descendants
     */
    children: function(selector) {
      var result = [],
          elem,
          i,
          j;
      for (i = 0, j = this.length; i < j; i++) {
        elem = this[i].firstChild;
        while (elem) {
          if (elem.nodeType === 1 && (!selector || _matchesSelector(elem, selector))) {
            result[result.length] = elem;
          }
          elem = elem.nextSibling;
        }
      }
      return new jMiny(result);
    },
    /**
     * @method each
     *
     * @signature Executes a callback for each element in this jMiny
     * @param<Function> callback(index,element) The function that will be executed for each element
     * @return<jMiny> This jMiny object
     */
    each: function(callback) {
      jMiny.each(this, callback);
      return this;
    },
    /**
     * @method eq
     *
     * @signature Gets a single element from this collection
     * @param<Number> index The index of the element
     * @return<jMiny> The element at the specified index as a jMiny object
     */
    eq: function(index) {
      return jMiny(this[index] || null);
    },
    /**
     * @method get
     *
     * @signature Gets a single element from this collection
     * @param<Number> index The index of the element
     * @return<Element> The DOM element at the specified index
     */
    get: function(index) {
      return this[index] || null;
    },
    /**
     * @method is
     *
     * @signature Determines whether or not this jMiny object matches a selector
     * @param<String> selector The CSS selector to test
     * @return<Boolean> True if the selector matches all of this jMiny's elements
     */
    is: function(selector) {
      var i = -1,
          elem;
      while (elem = this[++i]) {
        if (!_matchesSelector(elem, selector)) {
          return false;
        }
      }
      return this.length > 0;
    },
    /**
     * @method filter
     *
     * @signature Reduces this collection to elements matching a selector
     * @param<String> selector The CSS selector
     * @return<jMiny> A new collection containing the elements that matched
     */
    filter: function(selector, negate /* INTERNAL */) {
      var result = jMiny(),
          i = -1,
          elem,
          match;
      while (elem = this[++i]) {
        match = _matchesSelector(elem, selector);
        if (!selector || (negate ? !match : match)) {
          result.add(elem);
        }
      }
      return result;
    },
    /**
     * @method not
     *
     * @signature Reduces this collection to elements that do not match a selector
     * @param<String> selector The CSS selector
     * @return<jMiny> A new collection containing the elements that didn't match
     */
    not: function(selector) {
      return this.filter(selector, true);
    },
    /**
     * @method find
     *
     * @signature Queries descendants for elements matching a selector
     * @param<String> selector The CSS selector
     * @return<jMiny> A collection containing the matching descendants
     */
    find: function(selector) {
      var result = jMiny(),
          elem,
          i = -1;
      while (elem = this[++i]) {
        result.add(_findInDOM(selector, elem));
      }
      return result;
    },
    /**
     * @method first
     *
     * @signature Finds the first descendant
     * @return<jMiny> A collection containing the first descendants
     *
     * @signature Finds the first descendant matching a selector
     * @param<String> selector The CSS selector
     * @return<jMiny> A collection containing the first matching descendants
     */
    first: function(selector) {
      var i = -1,
          elem;
      while (elem = this[++i]) {
        if (!selector || _matchesSelector(elem, selector)) {
          return this.eq(i);
        }
      }
      return jMiny();
    },
    /**
     * @method last
     *
     * @signature Finds the last descendant
     * @return<jMiny> A collection containing the last descendants
     *
     * @signature Finds the last descendant matching a selector
     * @param<String> selector The CSS selector
     * @return<jMiny> A collection containing the last matching descendants
     */
    last: function(selector) {
      var i = this.length,
          elem;
      while (elem = this[--i]) {
        if (!selector || _matchesSelector(elem, selector)) {
          return this.eq(i);
        }
      }
      return jMiny();
    },
    /**
     * @method next
     *
     * @signature Finds the next sibling
     * @return<jMiny> A collection containing the next siblings
     *
     * @signature Finds the next sibling matching a selector
     * @param<String> selector The CSS selector
     * @return<jMiny> A collection containing the next matching siblings
     */
    next: function(selector) {
      var result = jMiny(),
          i = -1,
          elem,
          sib;
      while (elem = this[++i]) {
        sib = elem.nextSibling;
        if (sib && sib.nodeType === 1 && (!selector || _matchesSelector(sib, selector))) {
          result.add(sib);
        }
      }
      return result;
    },
    /**
     * @method prev
     *
     * @signature Finds the previous sibling
     * @return<jMiny> A collection containing the previous siblings
     *
     * @signature Finds the previous sibling matching a selector
     * @param<String> selector The CSS selector
     * @return<jMiny> A collection containing the previous matching siblings
     */
    prev: function(selector) {
      var result = jMiny(),
          i = -1,
          elem,
          sib;
      while (elem = this[++i]) {
        sib = elem.previousSibling;
        if (sib && sib.nodeType === 1 && (!selector || _matchesSelector(sib, selector))) {
          result.add(sib);
        }
      }
      return result;
    },
    /**
     * @method parent
     *
     * @signature Finds the direct parent element
     * @return<jMiny> A collection containing the direct parent element
     *
     * @signature Finds the direct parent element matching a selector
     * @param<String> selector A CSS selector
     * @return<jMiny> A collection of matching direct parents
     */
    parent: function(selector) {
      var result = jMiny(),
          i = -1,
          elem,
          parent;
      while (elem = this[++i]) {
        parent = elem.parentNode;
        if (parent && parent.nodeType === 1 && (!selector || _matchesSelector(parent, selector))) {
          result.add(parent);
        }
      }
      return result;
    },
    /**
     * @method parents
     *
     * @signature Finds all parent elements
     * @return<jMiny> A collection containing all parents
     *
     * @signature Finds all parent elements matching a selector
     * @param<String> selector A CSS selector
     * @return<jMiny> A collection containing all matching parents
     */
    parents: function(selector, closestOnly /* INTERNAL */) {
      var result = jMiny(),
          i = -1,
          elem,
          parent;
      while (elem = this[++i]) {
        while ((parent = elem.parentNode) && parent.nodeType === 1) {
          if (!selector || _matchesSelector(parent, selector)) {
            result.add(parent);
            if (closestOnly) {
              break;
            }
          }
        }
      }
      return result;
    },
    /**
     * @method closest
     *
     * @signature Finds the nearest parent element matching a selector
     * @param<String> selector A CSS selector
     * @return<jMiny> A collection containing the nearest matching parents
     */
    closest: function(selector) {
      return this.parents(selector, true);
    },
    // Attributes
    /**
     * @method attr
     *
     * @signature Gets the value of an attribute
     * @param<String> name The name of the attribute
     * @return<String> The value of the attribute
     *
     * @signature Sets the value of an attribute
     * @param<String> name The name of the attribute
     * @param<Object> value The value of the attribute
     * @return<jMiny> This collection
     */
    attr: function(name, value) {
      if (this.length) {
        if (arguments.length === 1) {
          return this[0].getAttribute(name);
        } else {
          var i = -1,
            elem;
          while (elem = this[++i]) {
            elem.setAttribute(name, value);
          }
          return this;
        }
      } else {
        return null;
      }
    },
    /**
     * @method removeAttr
     *
     * @signature Removes an attribute from elements in the collection
     * @param<String> name The name of the attribute to remove
     * @return<jMiny> This collection
     */
    removeAttr: function(name) {
      var i = -1,
          elem;
      while (elem = this[++i]) {
        elem.removeAttribute(name);
      }
      return this;
    },
    /**
     * @method addClass
     *
     * @signature Adds a class name to elements in this collection
     * @param<String> className The class to add
     * @return<jMiny> This collection
     */
    addClass: function(className) {
      var i = -1,
          exp = new RegExp('(^| )' + className + '($| )'),
          elem;
      while (elem = this[++i]) {
        if (!exp.test(elem.className)) {
          elem.className += ' ' + className;
        }
      }
      return this;
    },
    /**
     * @method hasClass
     *
     * @signature Tests if elements in the collection have a class name
     * @param<String> className The class
     * @return<Boolean> True if any element has the class name
     */
    hasClass: function(className) {
      var i = -1,
          exp = new RegExp('(^| )' + className + '($| )'),
          elem;
      while (elem = this[++i]) {
        if (exp.test(elem.className)) {
          return true;
        }
      }
      return false;
    },
    /**
     * @method removeClass
     *
     * @signature Removes a class from each element in the collection
     * @param<String> className The name of the class to remove
     * @return<jMiny> This collection
     */
    removeClass: function(className) {
      var i = -1,
          exp = new RegExp('(^| )' + className + '($| )'),
          elem;
      while (elem = this[++i]) {
        if (exp.test(elem.className)) {
          elem.className = elem.className.replace(exp, '');
        }
      }
      return this;
    },
    /**
     * @method toggleClass
     *
     * @signature Removes a class from elements that have it, otherwise adds it
     * @param<String> className The class to toggle
     * @return<jMiny> This collection
     */
    toggleClass: function(className) {
      var i = -1,
          exp = new RegExp('(^| )' + className + '($| )'),
          elem;
      while (elem = this[++i]) {
        if (exp.test(elem.className)) {
          elem.className = elem.className.replace(exp, '');
        } else {
          elem.className += ' ' + className;
        }
      }
      return this;
    },
    /**
     * @method css
     *
     * @signature Gets the value of a CSS property
     * @param<String> property The JavaScript name of the CSS property
     * @return<String> The value of the CSS property
     *
     * @signature Sets multiple CSS properties on the collection
     * @param<Object> properties A key-value map, where the keys are the JavaScript CSS property names and the values are those properties' new values
     * @return<jMiny> This collection
     *
     * @signature Sets a single CSS property on the collection
     * @param<String> property The JavaScript name of the CSS property
     * @param<String> value The value for the CSS property
     * @return<jMiny> This collection
     */
    css: function(property, value) {
      if (this.length) {
        if (typeof property == 'string' && arguments.length == 1) {
          return (window.getComputedStyle
            ? window.getComputedStyle(this[0], null)
            : document.defaultView.getComputedStyle(this[0], null))
              .getPropertyValue(property);
        } else {
          var opts = {},
              s = [],
              i = -1,
              p,
              elem;
          if (typeof property == 'object') {
            opts = jMiny.extend({}, property);
          } else if (typeof property == 'string' && arguments.length == 2) {
            opts[property] = value;
          }
          for (p in opts) {
            s.push(_translateCSSPropertyName(p), ':', opts[p], ';');
          }
          s = s.join('');
          while (elem = this[++i]) {
            elem.style.cssText += s;
          }
        }
      }
      return this;
    },
    /**
     * @method data
     *
     * @signature Gets the value of a data property
     * @param<String> property The name of the data property
     * @return<Object> The value of the data property
     *
     * @signature Sets multiple data properties on the collection
     * @param<Object> properties A key-value map, where the keys are the property names and the values are those properties' new values
     * @return<jMiny> This collection
     *
     * @signature Sets a single data property on the collection
     * @param<String> property The name of the data property
     * @param<Object> value The value for the data property
     * @return<jMiny> This collection
     */
    data: function(id, value) {
      var returnValue = arguments.length == 1 && typeof id == 'string',
          i = -1,
          elem,
          cache,
          options,
          n;
      if (this.length) {
        while (elem = this[++i]) {
          cache = elem['$cache' + _dataCacheId];
          if (!cache) {
            cache = elem['$cache' + _dataCacheId] = {};
            cache.toJSON = _doNothing;
          }
          if (returnValue) {
            return cache[id] || null;
          } else {
            options = arguments.length == 1 && typeof id == 'object' ? id : {};
            if (arguments.length == 2) {
              options[id] = value;
            }
            for (n in options) {
              cache[n] = options[n];
            }
          }
        }
        return this;
      } else {
        return returnValue ? null : this;
      }
    },
    /**
     * @method removeData
     *
     * @signature Removes a data property from the collection
     * @param<String> id The name of the data property
     * @return<jMiny> This collection
     */
    removeData: function(id) {
      var i = -1,
          elem,
          cache,
          n;
      while (elem = this[++i]) {
        cache = elem['$cache' + _dataCacheId];
        if (cache) {
          if (id) {
            delete cache[id];
          } else {
            delete elem['$cache' + _dataCacheId];
          }
        }
      }
      return this;
    },
    // Effects
    animate: function(prop, duration, easing, callback) {
      var me = this,
          cssProperties = {},
          transforms,
          prop;
      if (typeof duration == 'object') {
        easing = duration.easing;
        callback = duration.complete;
        duration = duration.duration;
      }
      if (typeof easing == 'function') {
        callback = easing;
        easing = null;
      }
      if (duration === UNDEFINED) {
        duration = 400;
      }
      for (name in prop) {
        if (name == 'transform') {
          cssProperties[_prefix + name] = prop[name];
        } else {
          cssProperties[name] = prop[name];
        }
        cssProperties[_prefix + 'transition'] = 'all ' + duration + 'ms ' + (easing || '');
      }
      function ontransitionend() {
        this.style[_prefix + 'transition'] = null;
        if (callback) {
          callback.call(this);
        }
      }
      if (duration > 0) {
        this.one(_transitionEndEventType, ontransitionend);
      }
      setTimeout(function() {
        me.css(cssProperties);
        if (duration <= 0) {
          setTimeout(function() {
            me.each(function() {
              ontransitionend.call(this);
            });
          }, 0);
        }
      }, 0);
      return this;
    },
    // DOM manipulation
    append: function(selector) {
      _domManip(selector, this, _appendElement);
      return this;
    },
    appendTo: function(selector) {
      return _domManip(this, selector, _appendElement);
    },
    prepend: function(selector) {
      _domManip(selector, this, _prependElement);
      return this;
    },
    prependTo: function(selector) {
       return _domManip(this, selector, _prependElement);
    },
    before: function(selector) {
      _domManip(selector, this, _insertBeforeElement);
      return this;
    },
    insertBefore: function(selector) {
      return _domManip(this, selector, _insertBeforeElement);
    },
    after: function(selector) {
      _domManip(selector, this, _insertAfterElement);
      return this;
    },
    insertAfter: function(selector) {
      return _domManip(this, selector, _insertAfterElement);
    },
    replaceAll: function(selector) {
      return _domManip(this, selector, _replaceElement);
    },
    replaceWith: function(selector) {
      return _domManip(selector, this, _replaceElement);
    },
    remove: function(selector) {
      if (selector) {
        this.find(selector).remove();
      } else {
        var i = -1,
            elem;
        while (elem = this[++i]) {
          if (elem.parentNode) {
            elem.parentNode.removeChild(elem);
          }
        }
      }
      return this;
    },
    clone: function(deepCopy) {
      var result = new jMiny(),
          i = -1,
          elem;
      while (elem = this[++i]) {
        result.add(elem.cloneNode(deepCopy));
      }
      return result;
    },
    empty: function() {
      return this.html('');
    },
    html: function(value) {
      if (typeof value == 'string') {
        var i = -1,
            elem;
        while (elem = this[++i]) {
          elem.innerHTML = value;
        }
        return this;
      } else {
        return this.length ? this[0].innerHTML : '';
      }
    },
    text: function(value) {
      if (typeof value == 'string') {
        var i = -1,
            elem;
        while (elem = this[++i]) {
          elem.innerText = value;
        }
        return this;
      } else {
        return this.length ? this[0].innerText : '';
      }
    },
    // Events
    on: function(type, delegate, callback) {
      if (arguments.length == 2) {
        callback = delegate;
        delegate = null;
      }
      var i = -1,
          elem;
      while (elem = this[++i]) {
        _addEventListener(elem, type, delegate, callback);
      }
    },
    off: function(type, delegate, callback) {
      if (arguments.length == 2) {
        callback = delegate;
        delegate = null;
      }
      var i = -1,
          elem;
      while (elem = this[++i]) {
        _removeEventListener(elem, type, delegate, callback);
      }
    },
    one: function(type, delegate, callback) {
      if (arguments.length == 2) {
        callback = delegate;
        delegate = null;
      }
      var i = -1,
          elem;
      while (elem = this[++i]) {
        _addEventListener(elem, type, delegate, callback, 1);
      }
    },
    // Geometry
    height: function() {
      return this.length ? this[0].offsetHeight : null;
    },
    width: function() {
      return this.length ? this[0].offsetWidth : null;
    },
    offset: function(coordinates) {
      if (coordinates) {
        var buffer = [],
            current,
            elem,
            parent,
            i = -1;
        while (elem = this[++i]) {
          current = jMiny(elem.parentNode).offset() || { left: 0, top: 0 };
          (function(elem, current, position) {
            buffer.push(function() {
              elem.style.cssText += ['position:', (position == 'static' ? 'relative' : position), ';left:', coordinates.left - current.left, 'px;top:', coordinates.top - current.top, 'px;right:auto;bottom:auto'].join('');
            });
          })(elem, current, elem.style.position);
        }
        i = -1;
        while (elem = buffer[++i]) {
          elem();
        }
        return this;
      } else {
        return this.length ? { left: this[0].offsetLeft, top: this[1].offsetTop } : null;
      }
    },
    offsetParent: function() {
      if (this.length) {
        var elem = this[0].parentNode;
        while (elem) {
          if (elem.style.position != 'static') {
            return jMiny(elem);
          }
          elem = elem.parentNode;
        }
        return jMiny(document.body);
      }
      return jMiny();
    },
    position: function() {
      if (this.length) {
        var elem = this[0],
            parent = elem.parentNode;
        return {
          left: elem.offsetLeft - (parent.offsetLeft || 0),
          top: elem.offsetTop - (parent.offsetTop || 0)
        };
      }
      return null;
    },
    scrollTop: function(value) {
      if (typeof value == 'number') {
        if (this.length) {
          this[0].scrollTop = value;
        }
        return this;
      } else {
        return this.length ? this[0].scrollTop : null;
      }
    },
    scrollLeft: function(value) {
      if (typeof value == 'number') {
        if (this.length) {
          this[0].scrollLeft = value;
        }
        return this;
      } else {
        return this.length ? this[0].scrollLeft : null;
      }
    },
    // Forms
    focus: function() {
      var i = -1,
          elem;
      while (elem = this[++i]) {
        elem.focus();
        break;
      }
      return this;
    },
    submit: function() {
      var i = -1,
          elem;
      while (elem = this[++i]) {
        if (elem.nodeName == 'FORM') {
          elem.submit();
        }
      }
      return this;
    },
    val: function(value) {
      var i = -1,
          elem,
          setMode = value !== UNDEFINED;
      while (elem = this[++i]) {
        switch (elem.nodeName) {
          case 'INPUT':
          case 'SELECT':
          case 'TEXTAREA':
            if (setMode) {
              elem.value = value;
              break;
            } else {
              return elem.value;
            }
        }
      }
      return setMode ? this : null;
    }
  };
  jMiny.fn = jMiny.prototype;
  jMiny.ajax = function(options) {
    var async = _defaultTo(options.async, true),
        cache = _defaultTo(options.cache, true),
        complete = options.complete,
        data = _defaultTo(options.data, null),
        dataType = _defaultTo(options.dataType, 'text'),
        error = options.error,
        success = options.success,
        timeout = options.timeout,
        type = _defaultTo(options.type, 'GET').toUpperCase(),
        url = options.url,
        xhr = new XMLHttpRequest();
    function oncomplete() {
      if (complete) {
        complete(this, this.statusText);
      }
      if (this.status === 200) {
        if (success) {
          var response;
          if (dataType == 'html') {
            response = _create(response)[0];
          } else if (dataType == 'xml') {
            response = this.responseXML;
          } else if (dataType == 'json') {
            response = JSON.parse(this.responseText);
          } else {
            response = this.responseText;
          }
          success(this, this.statusText, response);
        }
      } else {
        if (error) {
          error(this, this.statusText);
        }
      }
    }
    if (!cache) {
      url += (url.indexOf('?') > -1 ? '&' : '?') + '_=' + new Date().getTime();
    }
    if (data) {
      if (typeof data == 'object') {
        data = jMiny.param(data);
      }
      if (type === 'GET') {
        url += (url.indexOf('?') > -1 ? '&' : '?') + data;
      }
    }
    xhr.open(type, url, async);
    if (timeout !== UNDEFINED) {
      xhr.timeout = timeout;
    }
    if (async) {
      xhr.onreadystatechange = function() {
        if (this.readyState === 4) {
          oncomplete.call(this);
        }
      };
    }
    xhr.send(type === 'GET' ? null : data);
    if (!async) {
      oncomplete.call(xhr);
    }
    xhr = null;
  };
  jMiny.each = function(list, callback) {
    var item,
        i = -1,
        j = list.length;
    while (++i < j) {
      callback.call(list, callback, i, item);
    }
  };
  jMiny.extend = function(subject, extensions) {
    for (var n in extensions) {
      subject[n] = extensions[n];
    }
    return subject;
  };
  jMiny.map = function(list, callback) {
    var result = [],
        i = -1;
    while ((result[++i] = callback.call(list, i, list[i])) !== UNDEFINED) {
      // Do nothing
    }
    result.length--;
    return result;
  };
  jMiny.param = function(obj) {
    var output = [],
        n;
    for (n in obj) {
      output.push(encodeURIComponent(n), '=', encodeURIComponent(obj[n]));
    }
    return output.join('&');
  };
  jMiny.proxy = function(callback, thisp) {
    return function() {
      callback.apply(thisp, arguments);
    };
  };
  jMiny.toArray = function(list) {
    return Array.prototype.splice.call(list, 0);
  };
  jMiny.trim = function(str) {
    return str ? str.replace(/(^\s+)|(\s$)/g, '') : str;
  };
  jMiny.noConflict = function(all) {
    if (all) {
      window['jMiny'] = _oldNS;
    }
    window['$'] = _old$;
    return jMiny;
  };
  
  window['jMiny'] = window['$'] = jMiny;

})();